package com.apobates.forum.utils;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NavigableMap;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.jsoup.Jsoup;
import org.jsoup.safety.Whitelist;

/**
 * 公共工具类
 *
 * @author xiaofanku
 * @since 20200510
 */
public final class Commons {
    public final static String NEWLINE = "\r\n";
    private final static NavigableMap<Long, String> suffixes = new TreeMap<>();
    private final static Pattern MAIL_REG = Pattern.compile("^[\\w!#$%&'*+/=?`{|}~^-]+(?:\\.[\\w!#$%&'*+/=?`{|}~^-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}$");
    static {
        suffixes.put(1_000L, "k");
        suffixes.put(1_000_000L, "M");
        suffixes.put(1_000_000_000L, "G");
        suffixes.put(1_000_000_000_000L, "T");
        suffixes.put(1_000_000_000_000_000L, "P");
        suffixes.put(1_000_000_000_000_000_000L, "E");
    }
    private Commons() throws Exception {
        throw new Exception("不需要实化公共工具类");
    }
    /**
     * 判断参数是否不为空
     *
     * @param str
     * @return
     */
    public static boolean isNotBlank(String str) {
        boolean data = StringUtils.isNotBlank(str);
        if (data) {
            data = !(str.toLowerCase().equals("null"));
        }
        return data;
    }
    /**
     * 获得用户远程地址
     * getRemoteAddr的别名方法
     *
     * @param request
     * @return
     */
    public static String getRequestIp(HttpServletRequest request) {
        return getRemoteAddr(request);
    }
    /**
     * 获得用户远程地址
     *
     * @param request
     * @return
     */
    public static String getRemoteAddr(HttpServletRequest request) {
        String remoteAddr = request.getHeader("X-Real-IP");
        if (isNotBlank(remoteAddr)) {
            remoteAddr = request.getHeader("X-Forwarded-For");
        } else if (isNotBlank(remoteAddr)) {
            remoteAddr = request.getHeader("Proxy-Client-IP");
        } else if (isNotBlank(remoteAddr)) {
            remoteAddr = request.getHeader("WL-Proxy-Client-IP");
        }
        return null != remoteAddr ? remoteAddr : request.getRemoteAddr();
    }
    /**
     * 如果目标为空或null使用默认值
     *
     * @param target 目标字符串值
     * @param defaultValue 默认的值
     * @return
     */
    public static String optional(String target, String defaultValue) {
        if(!isNotBlank(target)){
            return defaultValue;
        }
        return target;
    }
    /**
     * 如果目标为空或null使用默认值
     * 
     * @param target 目标可能会为Null的供应函数
     * @param defaultValue 默认的值
     * @return 
     */
    public static String optional(Supplier<Object> target, String defaultValue){
        try{
            return optional(target.get().toString(), defaultValue);
        }catch(NullPointerException e){
            return defaultValue;
        }
    }
    /**
     * 如果目标为空或null使用默认值
     *
     * @param target 目标值
     * @param defaultValue 默认值
     * @return
     */
    public static Long optional(Long target, Long defaultValue) {
        if (null != target && target > 0) {
            return target;
        }
        return defaultValue;
    }
    /**
     * 如果目标为空或null使用默认值为0
     *
     * @param target 目标值
     * @return
     */
    public static Long optionalOrZero(Long target) {
        return optional(target, 0L);
    }
    /**
     * Http protocol是否是Https,是返回true
     *
     * @param sitedomain 域名
     * @return 若参数不包含Http protocol信息返回false, 若参数不可用返回false
     */
    public static boolean isHttpsProtocol(String sitedomain) {
        if (!isNotBlank(sitedomain)) {
            return false;
        }
        try {
            String protocol = sitedomain.substring(0, sitedomain.indexOf(":"));
            return protocol.toLowerCase().equals("https");
        } catch (IndexOutOfBoundsException e) {}
        return false;
    }
    /**
     * 生成对象的json字符串
     * 
     * @param <T>
     * @param t
     * @return 
     */
    public static <T> String toJson(T t) {
        Objects.requireNonNull(t);
        return new Gson().toJson(t);
    }
    /**
     * 生成Map的json字符串
     * 
     * @param map
     * @return 
     */
    public static String toJson(Map<String,String> map) {
        if(null == map || map.isEmpty()){
            return "{}";
        }
        Type gsonType = new TypeToken<Map<String,String>>(){}.getType();
        return new Gson().toJson(map, gsonType);
    }
    public static String toStatsJson(Map<String,Long> map) {
        if(null == map || map.isEmpty()){
            return "{}";
        }
        Type gsonType = new TypeToken<Map<String,Long>>(){}.getType();
        return new Gson().toJson(map, gsonType);
    }
    /**
     * 将json字符串还原为Map
     * 
     * @param jsonString
     * @return 
     */
    public static Map<String,String> fromString(String jsonString){
        Objects.requireNonNull(jsonString);
        return new Gson().fromJson(jsonString, new TypeToken<Map<String, String>>() {}.getType());
    }
    public static Map<String,Long> fromStatsString(String jsonString){
        Objects.requireNonNull(jsonString);
        return new Gson().fromJson(jsonString, new TypeToken<Map<String, Long>>() {}.getType());
    }
    /**
     * 将字符串参数进行BASE64编码
     * @param str 待编码的字符串
     * @param failValue 编码失败或字符串为null时的操作
     * @return 
     */
    public static String base64Encode(String str, Supplier<String> failValue){
        Objects.requireNonNull(str);
        return CookieUtils.encodeCookieValue(str, failValue.get());
    }
    /**
     * 对字符串参数进行BASE64解码
     * @param str 待解码的字符串
     * @param failValue 解码失败或字符串为null时的操作
     * @return 
     */
    public static String base64Decode(String str, Supplier<String> failValue){
        Objects.requireNonNull(str);
        return CookieUtils.decodeCookieValue(str, failValue.get());
    }
    /**
     * 将字符串参数进行DES加密
     * @param secretString 公钥. 不可以为null,反之抛出NPE
     * @param str 待加密的字符串. 不可以为null,反之抛出NPE
     * @param failValue 加密失败时的操作
     * @return 
     */
    public static String desEncrypt(String secretString, String str, Supplier<String> failValue){
        Objects.requireNonNull(secretString); 
        Objects.requireNonNull(str);
        String data;
        try {
            TripleDesCryp dcs = new TripleDesCryp(secretString);
            data = dcs.encrypt(str);
        } catch (Exception e) {
            data = failValue.get();
        }
        return data;
    }
    /**
     * 对字符串参数进行DES解密
     * @param secretString 公钥. 不可以为null,反之抛出NPE
     * @param str 待解密的字符串. 不可以为null,反之抛出NPE
     * @param failValue 解密失败时的操作
     * @return 
     */
    public static String desDecrypt(String secretString, String str, Supplier<String> failValue){
        Objects.requireNonNull(secretString); 
        Objects.requireNonNull(str);
        String data;
        try {
            TripleDesCryp dcs = new TripleDesCryp(secretString);
            data = dcs.decrypt(str);
        } catch (Exception e) {
            data = failValue.get();
        }
        return data;
    }
    public static void copyProperties(Object dest, Object orig) throws IllegalAccessException, InvocationTargetException{
        BeanUtils.copyProperties(dest, orig);
    }
    /**
     * 用单引号括住集合中的每一个字符串
     *
     * @param params 参数必须是字符串,不适用于数字字符串集合
     * @return 若参数为null或集合是空的返回null
     */
    public static Optional<String> quoteSQLParameters(Collection<String> params) {
        if (null == params || params.isEmpty()) {
            return Optional.empty();
        }
        String data = String.join("','", params);
        return Optional.of(String.format("'%s'", data));
    }
    /**
     * 将字符串转换成安全的字符.只有纯文字,所有标签都被剔除
     * 使用org.jsoup实现
     *
     * @param htmlString
     * @return
     */
    public static String htmlPurifier(String htmlString) {
        Objects.requireNonNull(htmlString);
        return Jsoup.clean(htmlString, Whitelist.none());
    }
    /**
     * 替掉不安全的html标签,最大程度的保留格式标签.可用的标签如下
     * a, b, blockquote, br, caption, cite, code, col, colgroup, dd, div, dl,
     * dt, em, h1, h2, h3, h4, h5, h6, i, img, li, ol, p, pre, q, small, span,
     * strike, strong, sub, sup, table, tbody, td, tfoot, th, thead, tr, u,
     * ul
     * 使用org.jsoup实现
     *
     * @param htmlString
     * @return
     */
    public static String htmlCleanUsedRelaxed(String htmlString) {
        Objects.requireNonNull(htmlString);
        return Jsoup.clean(htmlString, Whitelist.relaxed());
    }
    /**
     * 是否是数字字符串
     * 只适用于正整数,不适用于小数位的字符串,不适用于负数
     * 使用apache common lang3实现
     * Checks if the CharSequence contains only Unicode digits. A decimal point
     * is not a Unicode digit and returns false.
     *
     * @param str
     * @return
     */
    public static boolean isNumeric(String str) {
        Objects.requireNonNull(str);
        return StringUtils.isNumeric(str);
    }
    /**
     * sha256 散列
     * 使用org.apache.commons.codec实现
     *
     * @param unEncryptString 未加密的字符串
     * @param salt 盐
     * @return
     */
    public static String sha256(String unEncryptString, String salt) {
        Objects.requireNonNull(unEncryptString);
        Objects.requireNonNull(salt);
        String data = salt + unEncryptString;
        return DigestUtils.sha256Hex(data);
    }
    /**
     * md5 散列
     * 使用org.apache.commons.codec实现
     * 
     * @param unEncryptString 未加密的字符串
     * @return 
     */
    public static String md5(String unEncryptString){
        Objects.requireNonNull(unEncryptString);
        return DigestUtils.md5Hex(unEncryptString);
    }
    /**
     * Whats the difference between Introspector vs just reflection?
     * Introspection uses reflection to do its work, but works at a higher level than reflection.Reflection finds Java-level fields and methods; introspection finds JavaBeans-level properties and events.
     * @see https://stackoverflow.com/questions/6796187/java-introspection-object-to-map
     * 
     * @param obj
     * @return
     * @throws java.beans.IntrospectionException
     * @throws java.lang.IllegalAccessException
     * @throws java.lang.reflect.InvocationTargetException
     */
    public static Map<String, Object> introspect(Object obj) throws IntrospectionException, IllegalAccessException, IllegalArgumentException, InvocationTargetException{
        Map<String, Object> result = new HashMap<>();
        BeanInfo info = Introspector.getBeanInfo(obj.getClass());
        for (PropertyDescriptor pd : info.getPropertyDescriptors()) {
            Method reader = pd.getReadMethod();
            if (reader != null) {
                result.put(pd.getName(), reader.invoke(obj));
            }
        }
        return result;
    }
    /**
     * 生成指定数量的数机字符,字母和数字
     * 使用org.apache.commons.lang3实现
     *
     * @param size 随机数的长度
     * @return
     */
    public static String randomAlphaNumeric(int size) {
        return RandomStringUtils.randomAlphanumeric(size);
    }
    /**
     * 将指定的字符串转化成Integer
     * 
     * @param value 待转化的字符串
     * @param defaultValue 转化失败时的默认值
     * @return 
     */
    public static int stringToInteger(String value, int defaultValue) {
        return stringToNumberic(value, val->Integer.valueOf(val.trim()), ()->defaultValue);
    }
    /**
     * 将指定的字符串转化成Integer
     * 
     * @param value 目标取值函数
     * @param defaultValue 转化失败时的默认值
     * @return 
     */
    public static int stringToInteger(Supplier<String> value, int defaultValue){
        try{
            return stringToInteger(value.get(), defaultValue);
        }catch(Exception e){
            return defaultValue;
        }
    }
    /**
     * 
     * @param <R>
     * @param value
     * @param mapper
     * @param supplier
     * @return 
     */
    private static <R> R stringToNumberic(String value, Function<String, R> mapper, Supplier<R> supplier){
        try {
            return Optional.ofNullable(value).map(mapper).orElseGet(supplier);
        } catch (NumberFormatException e) {
            return supplier.get();
        }
    }
    /**
     * 将指定的字符串转化成Long
     * 
     * @param value 待转化的字符串
     * @param defaultValue 转化失败时的默认值
     * @return 
     */
    public static long stringToLong(String value, long defaultValue) {
        return stringToNumberic(value, val->Long.valueOf(val.trim()), ()->defaultValue);
    }
    /**
     * 将指定的字符串转化成Long
     * 
     * @param value 目标取值函数
     * @param defaultValue 转化失败时的默认值
     * @return 
     */
    public static long stringToLong(Supplier<String> value, long defaultValue) {
        try{
            return stringToLong(value.get(), defaultValue);
        }catch(Exception e){
            return defaultValue;
        }
    }
    /**
     * 将指定的字符串转化成Double
     * 
     * @param value 目标取值函数
     * @param defaultValue 转化失败时的默认值
     * @return 
     */
    public static double stringToDouble(Supplier<String> value, double defaultValue) {
        try{
            return stringToNumberic(value.get(), val->Double.valueOf(val.trim()), ()->defaultValue);
        }catch(Exception e){
            return defaultValue;
        }
    }
    /**
     * 将指定的字符串转化成Float
     * 
     * @param value 待转化的字符串
     * @param defaultValue 转化失败时的默认值
     * @return 
     */
    public static Float stringToFloat(String value, Float defaultValue){
        return stringToNumberic(value, val->Float.valueOf(val.trim()), ()->defaultValue);
    }
    /**
     * 将逗号分隔的数字字符串转成Long列表
     *
     * @param idArrayString
     * @return
     */
    public static List<Long> toLongList(String idArrayString) {
        if (!Commons.isNotBlank(idArrayString)) {
            return Collections.emptyList();
        }
        return Stream.of(idArrayString.split(",")).filter(s -> s.trim().length() > 0 && NumberUtils.isCreatable(s.trim())).map(s -> Long.parseLong(s.trim())).distinct().collect(Collectors.toList());
    }
    /**
     * 将逗号分隔的数字字符串转成Long集合
     * 
     * @param idArrayString
     * @return 
     */
    public static Set<Long> toLongSet(String idArrayString) {
        if (!Commons.isNotBlank(idArrayString)) {
            return Collections.emptySet();
        }
        return Stream.of(idArrayString.split(",")).filter(s -> s.trim().length() > 0 && NumberUtils.isCreatable(s.trim())).map(s -> Long.parseLong(s.trim())).distinct().collect(Collectors.toSet());
    }
    /**
     *
     * @param url 处理的连接地址
     * @param domain 域名,需要带有http[s]//
     * @param defaultURL 失败时的默认值
     * @return
     */
    public static String getNativeURL(String url, String domain, String defaultURL) {
        if (isNotBlank(url) && url.startsWith(domain)) {
            try {
                URL nu = new URL(url);
                return nu.getFile();
            } catch (MalformedURLException e) {}
        }
        return defaultURL;
    }
    /**
     * 只保留参数中的字母和数字,其它的都删除
     *
     * @param value 参数值
     * @return 如果参数等于null或空字符返回空字符串
     */
    public static String getAlphaNumberCharacter(String value) {
        if (Commons.isNotBlank(value)) {
            return value.replaceAll("[^a-zA-Z0-9]", "");
        }
        return "";
    }
    private static boolean isLetter(char c) {
        int k = 0x80;
        return c / k == 0;
    }
    /**
     * 中文占2个,大写,小写,空格和数字占1个
     *
     * @param string null输出0
     * @return
     */
    public static int chineseStringLength(String string) {
        if (null == string) {
            return 0;
        }
        char[] c = string.toCharArray();
        int len = 0;
        for (int i = 0; i < c.length; i++) {
            len++;
            if (!isLetter(c[i])) {
                len++;
            }
        }
        return len;
    }
    /**
     * 生成指定数量的随机数字
     * 使用org.apache.commons.lang3实现
     *
     * @param size 随机数的长度
     * @return
     */
    public static String randomNumeric(int size) {
        return RandomStringUtils.randomNumeric(size);
    }
    /**
     * 生成指定数量的随机数字
     * 使用org.apache.commons.lang3实现
     *
     * @param minSize 随机数的最短长度
     * @param maxSize 随机数的最长长度
     * @return
     */
    public static String randomNumeric(int minSize, int maxSize) {
        int size = RandomUtils.nextInt(minSize, maxSize);
        return RandomStringUtils.randomNumeric(size);
    }
    /**
     * 在指定范围内随机取一个字符串
     *
     * @param start 开始点
     * @param length 最大值
     * @return
     */
    public static long randomNumericRange(long start, long length) {
        long seek = RandomUtils.nextLong(start, length);
        return seek;
    }
    /**
     * 格式化数字,例: 101800 => 101k
     * {@link https://stackoverflow.com/questions/4753251/how-to-go-about-formatting-1200-to-1-2k-in-java}
     *
     * @param value 被格式化的数字
     * @return
     */
    public static String longNumbericFormat(long value) {
        // Long.MIN_VALUE == -Long.MIN_VALUE so we need an adjustment here
        if (value == Long.MIN_VALUE) {
            return longNumbericFormat(Long.MIN_VALUE + 1);
        }
        if (value < 0) {
            return "-" + longNumbericFormat(-value);
        }
        if (value < 1000) {
            return Long.toString(value); // deal with easy case
        }
        Entry<Long, String> e = suffixes.floorEntry(value);
        Long divideBy = e.getKey();
        String suffix = e.getValue();
        
        long truncated = value / (divideBy / 10); // the number part of the
        // output times 10
        boolean hasDecimal = truncated < 100 && (truncated / 10d) != (truncated / 10);
        return hasDecimal ? (truncated / 10d) + suffix : (truncated / 10) + suffix;
    }
    /**
     * 验证邮箱地址
     *
     * @see https://howtodoinjava.com/regex/java-regex-validate-email-address/
     * @param mailString 邮箱地址
     * @return
     */
    public static boolean isMailString(String mailString) {
        if (!isNotBlank(mailString)) {
            return false;
        }
        return MAIL_REG.matcher(mailString).matches();
    }
    /**
     * 获取文件的扩展名,使用ASF Commons IO FilenameUtils
     *
     * @param filename 文件名或路径字符
     * @return 失败会返回null或空字符串
     */
    public static String getFileExtension(String filename) {
        return FilenameUtils.getExtension(filename);
    }
    /**
     * 使用InetAddress类的hashcode来实现
     * 
     * @param ipAddress 若参数不可用返回0
     * @return 若有异常发生会返回-1; 
     */
    public static int ipHashcode(String ipAddress){
        if(!Commons.isNotBlank(ipAddress)){
            return 0;
        }
        try{
            InetAddress address = InetAddress.getByName(ipAddress);
            return address.hashCode();
        }catch(UnknownHostException e){
            return -1;
        }
    }
    /**
     * 合并集合
     * 
     * @param <K>
     * @param <V>
     * @param baseMap 以此集合为基准
     * @param extMap 在基准的集合上合并此集合
     * @param remappingFunction 合并时的运作函数
     * @return 
     */
    public static <K,V> Map<K,V> mergeMap(Map<K,V> baseMap, Map<K,V> extMap, BiFunction<? super V, ? super V, ? extends V> remappingFunction){
        if(null == extMap || extMap.isEmpty()){
            return baseMap;
        }
        Map<K, V> data = new HashMap<>(baseMap);
        extMap.forEach((newKey, newVal)->{
            data.merge(newKey, newVal, remappingFunction);
        });
        return data;
    }
    /**
     * 执行Stream的收集,使用Collectors.toMap
     * 
     * @param <T> 流的类型
     * @param <K> 收集后的集合Key类型
     * @param <V> 收集后的集合Key类型
     * @param src 源流
     * @param keyMaper key的映射函数,注要重复性
     * @param valueMapper value的映射函数
     * @return 
     */
    public static <T,K,V> Map<K,V> collectMap(Stream<T> src, Function<T, K> keyMaper, Function<T, V> valueMapper){
        return src.collect(Collectors.toMap(keyMaper, valueMapper));
    }
}